<!DOCTYPE html>
<html>
    
<head>
    <meta charset="utf-8">
    
    <title>Android进阶基础系列：View的工作原理 全面理解！ | ApocalypseBlog</title>
    <canvas id="header_canvas"style="position:absolute;bottom:0"></canvas> 
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    

    

    

    

    
<link rel="stylesheet" href="/dist/build.css?v=1.14.0.css">

    <script src="/javascripts/bubble.js"></script>
    <script>
        window.isPost = true
        window.aomori = {
            
            
        }
        window.aomori_logo_typed_animated = false
        window.aomori_search_algolia = false

    </script>
<script>
    ((window.gitter = {}).chat = {}).options = {
      room: 'ApocalypseBlog/Apocalypse'
    };
  </script>
  <script src="https://sidecar.gitter.im/dist/sidecar.v1.js" async defer></script>
<meta name="generator" content="Hexo 5.3.0"></head>
<!--DaoVoice服务http://blog.daovoice.io/-->

    <script>(function (i, s, o, g, r, a, m) {
        i['DaoVoiceObject'] = r;
        i[r] = i[r] ||
          function () {
            (i[r].q = i[r].q || []).push(arguments);
          };
        i[r].l = 1 * new Date();
        a = s.createElement(o);
        m = s.getElementsByTagName(o)[0];
        a.async = 1;
        a.src = g;
        a.charset = 'utf-8';
        m.parentNode.insertBefore(a, m);
      })(window, document, 'script', ('https:' === document.location.protocol ? 'https:' : 'http:') + "//widget.daovoice.io/widget/b00f5052.js", 'daovoice');
      daovoice('init', {
        app_id: "b00f5052",
      });
      daovoice('update');
    </script>
  
<body>

    <div class="container">
    <header class="header">
        <div class="header-type">
            
            <div class="header-type-inner">
                
                    <a class="header-type-title" href="/">ApocalypseBlog</a>
                
    
                
            </div>
        </div>
        <div class="header-menu">
            <div class="header-menu-inner">
                
            </div>
            <div class="header-menu-social">
                
            </div>
        </div>

        <div class="header-menu-mobile">
            <div class="header-menu-mobile-inner" id="mobile-menu-open">
                <i class="icon icon-menu"></i>
            </div>
        </div>
    </header>

    <div class="header-menu-mobile-menu">
        <div class="header-menu-mobile-menu-bg"></div>
        <div class="header-menu-mobile-menu-wrap">
            <div class="header-menu-mobile-menu-inner">
                <div class="header-menu-mobile-menu-close" id="mobile-menu-close">
                    <i class="icon icon-cross"></i>
                </div>
                <div class="header-menu-mobile-menu-list">
                    
                </div>
            </div>
        </div>
    </div>

</div>

    <div class="container">
        <div class="main">
            <section class="inner">
                <section class="inner-main">
                    <div class="post">
    <article id="post-ckk6o6awz00177kny32g7fd95" class="article article-type-post" itemscope
    itemprop="blogPost">

    <div class="article-inner">

        
          
        
        
        

        
        <header class="article-header">
            
  
    <h1 class="article-title" itemprop="name">
      Android进阶基础系列：View的工作原理 全面理解！
    </h1>
  

        </header>
        

        <div class="article-more-info article-more-info-post hairline">

            <div class="article-date">
  <time datetime="2020-07-25T11:03:07.000Z" itemprop="datePublished">2020-07-25</time>
</div>

            

            

            

        </div>

        <div class="article-entry post-inner-html hairline" itemprop="articleBody">
            <h1 id="1、ViewRoot-和-DecorView"><a href="#1、ViewRoot-和-DecorView" class="headerlink" title="1、ViewRoot 和 DecorView"></a>1、ViewRoot 和 DecorView</h1><ol>
<li><strong>ViewRoot对应ViewRootImpl类，是连接WindowManager和DecorView的纽带。View的三大流程是通过ViewRoot完成的。</strong> 在ActivityThread中，当Activity对象被创建完毕时，会将DecorView添加到Window中，同时会创建ViewRootImpl，且ViewRootImpl和DecorView会建立关联。如下代码，WindowManagerGlobal的addView()方法：</li>
</ol>
<a id="more"></a>

<pre><code class="java">public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) &#123;
            ...
            root = new ViewRootImpl(view.getContext(), display);
            root.setView(view, wparams, panelParentView);
            ...
            &#125;</code></pre>
<ol>
<li>View绘制流程从 <strong>performTraversals</strong>开始，经过Measure、layout、draw。流程图如下</li>
</ol>
<p><img src="http://user-gold-cdn.xitu.io/2020/7/20/1736bb003147c6d3?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="在这里插入图片描述"></p>
<p>\3. <strong>DecorView</strong>是顶级View，是一个FrameLayout，上面是标题栏、下面是内容栏。内容栏就是setContengView的内容view，id是content。事件 经过DecorView 然后传给我们自己的View。</p>
<h1 id="2、-MeasureSpec"><a href="#2、-MeasureSpec" class="headerlink" title="2、 MeasureSpec"></a>2、 MeasureSpec</h1><blockquote>
<p>MeasureSpec封装了从父级传递到子级的布局要求。<em>系统把view的LayoutParams 根据 父容器施加的规则（父容器的SpecMode） 转换成 view的MeasureSpec，然后使用这个MeasureSpec确定view的<strong>测量宽高</strong>（不一定是最终宽高）。</em></p>
</blockquote>
<h2 id="2-1MeasureSpec"><a href="#2-1MeasureSpec" class="headerlink" title="2.1MeasureSpec"></a>2.1MeasureSpec</h2><p>1.<strong>MeasureSpec</strong>—view的测量规格：高2位的SpecMode，低30位的SpecSize。 2.<strong>SpecMode的分类</strong>： UNPECIFIED，父容器对view不限制，要多大给多大，一般系统内部使用。 EXACTLY，父容器检测出view所需大小，view最终大小就是SpecSize的值。对应 LayoutParams中的matchParent、具体数值 两种模式。 AT_MOST，父容器制定了可用大小即SpecSize，view的大小不能大于这个值，具体要看view的具体实现。对应LayoutParams中的wrap_content。</p>
<h2 id="2-2MeasureSpec和LayoutParams的对应关系"><a href="#2-2MeasureSpec和LayoutParams的对应关系" class="headerlink" title="2.2MeasureSpec和LayoutParams的对应关系"></a>2.2MeasureSpec和LayoutParams的对应关系</h2><blockquote>
<p>前面说了<strong>View的MeasureSpec是由LayoutParams和父容器的MeasureSpec共同决定</strong>。顶级view，即<strong>DecorView，是由窗口尺寸和自身LayoutParams决定</strong>。</p>
</blockquote>
<p>1、<strong>DecorView</strong>，ViewRootImpl中measureHierarchy()方法（performTraversals中执行），代码如下，desiredWindowWidth、desiredWindowHeight是屏幕的尺寸。</p>
<pre><code class="java">private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) &#123;
            ...
                childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            ...
            &#125;</code></pre>
<p>performMeasure()内部是调用mView.measure(childWidthMeasureSpec, childHeightMeasureSpec)，mView就是DecorVIew。继续看getRootMeasureSpec（）方法如下：</p>
<pre><code class="java">/**
     * Figures out the measure spec for the root view in a window based on it&#39;s
     * layout params.
     *
     * @param windowSize
     *            The available width or height of the window
     *
     * @param rootDimension
     *            The layout params for one dimension (width or height) of the
     *            window.
     *
     * @return The measure spec to use to measure the root view.
     */
    private static int getRootMeasureSpec(int windowSize, int rootDimension) &#123;
        int measureSpec;
        switch (rootDimension) &#123;

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can&#39;t resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        &#125;
        return measureSpec;
    &#125;</code></pre>
<p>DecorView的MeasureSpec就明确了，根据其LayoutParams：</p>
<ul>
<li>MATCH_PARENT：精确模式，就是窗口大小；</li>
<li>WRAP_CONTENT：最大值模式，最大值不能超过窗口大小；</li>
<li>固定值（如100dp）：精确模式，就是LayoutParams的指定值。</li>
</ul>
<p>2、<strong>普通View</strong>，测量过程从ViewGroup传递下来，看ViewGroup的measureChildWithMargins()方法：</p>
<pre><code class="java">/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) &#123;
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    &#125;</code></pre>
<p>即先获取child的MeasureSpec，再调child.measure()。可以看到，child的MeasureSpec是由父容器的﻿﻿MeasureSpec、父容器的padding、child的LayoutParams、child的marging 共同决定。继续看getChildMeasureSpec()方法：</p>
<pre><code class="java">/**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) &#123;
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        //padding,就是已被占用的空间，就是 父容器的padding+child的marging
        //size，是ViewGroup本身size减去已使用的空间，是ViewGroup能提供给child的最大值。
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) &#123;
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension &gt;= 0) &#123;
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            &#125; else if (childDimension == LayoutParams.MATCH_PARENT) &#123;
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            &#125; else if (childDimension == LayoutParams.WRAP_CONTENT) &#123;
                // Child wants to determine its own size. It can&#39;t be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            &#125;
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension &gt;= 0) &#123;
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            &#125; else if (childDimension == LayoutParams.MATCH_PARENT) &#123;
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            &#125; else if (childDimension == LayoutParams.WRAP_CONTENT) &#123;
                // Child wants to determine its own size. It can&#39;t be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            &#125;
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension &gt;= 0) &#123;
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            &#125; else if (childDimension == LayoutParams.MATCH_PARENT) &#123;
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            &#125; else if (childDimension == LayoutParams.WRAP_CONTENT) &#123;
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            &#125;
            break;
        &#125;
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    &#125;</code></pre>
<p>可见，view的MeasureSpec由 viewParent的MeasureSpec和自身layoutParams确定。另外，child的可利用的尺寸是parent尺寸减去padding，上面代码已有注释，这很好理解。 梳理如下：</p>
<table>
<thead>
<tr>
<th>parentSpecMode /childLayoutParams</th>
<th>EXACTLY</th>
<th>AT_MOST</th>
<th>UNSPECIFIED</th>
</tr>
</thead>
<tbody><tr>
<td>dp/px</td>
<td>EXACTLY childSize</td>
<td>EXACTLY childsize</td>
<td>EXACTLY childsize</td>
</tr>
<tr>
<td>match_parent</td>
<td>EXACTLY parentSize</td>
<td>AT_MOST parentSize</td>
<td>UNSPECIFIED 0</td>
</tr>
<tr>
<td>wrap_content</td>
<td>AT_MOST parentSize</td>
<td>AT_MOST parentSize</td>
<td>UNSPECIFIED 0</td>
</tr>
</tbody></table>
<p>注意，parentSize是父容器可使用的大小。</p>
<p><strong>更新</strong>，看到鸿洋公众号的文章关于<strong>UNSPECIFIED</strong>说明：</p>
<blockquote>
<p><strong>MeasureSpec.UNSPECIFIED是不是真的不常见？</strong></p>
</blockquote>
<blockquote>
<p>在日常定制View时，确实很少会专门针对这个模式去做特殊处理，大多数情况下，都会把它当成MeasureSpec.AT_MOST一样看待，就比如最最常用的TextView，它在测量时也是不会区分UNSPECIFIED和AT_MOST的。</p>
</blockquote>
<blockquote>
<p>不过，虽说这个模式比较少直接接触到，但很多场景下，我们已经在不知不觉中用上了，比如<strong>RecyclerView的Item</strong>，如果Item的宽/高是wrap_content且列表可滚动的话，那么Item的宽/高的测量模式就会是UNSPECIFIED。 还有就是<strong>NestedScrollView</strong>和<strong>ScrollView</strong>，因为它们都是扩展自FrameLayout，所以它们的子View会测量两次，第一次测量时，子View的heightMeasureSpec的模式是写死为UNSPECIFIED的。 我们在自定义ViewGroup过程中，如果允许子View的尺寸比ViewGroup大的话，在测量子View时就可以把Mode指定为UNSPECIFIED。</p>
</blockquote>
<blockquote>
<p>看到ScrollView重写了measureChild方法，指定高度的mode是UNSPECIFIED</p>
<p>![ScrollView重写了measureChild方法，指定高度的mode是UNSPECIFIED](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1280" height="537"></svg>)</p>
</blockquote>
<h1 id="3、View的工作流程"><a href="#3、View的工作流程" class="headerlink" title="3、View的工作流程"></a>3、View的工作流程</h1><blockquote>
<p><strong>View的三大流程，measure、layout、draw</strong>。measure确定view的测量宽高，layout确定view的最终宽高和四个顶点位置，draw绘制到屏幕。</p>
</blockquote>
<h2 id="3-1-Measure过程"><a href="#3-1-Measure过程" class="headerlink" title="3.1 Measure过程"></a>3.1 Measure过程</h2><p>view的测量过程，由measure()方法完成。viewGroup测量自身后，还需调用child.measure()遍历测量子view。</p>
<h3 id="3-1-1-view的测量过程"><a href="#3-1-1-view的测量过程" class="headerlink" title="3.1.1 view的测量过程"></a>3.1.1 view的测量过程</h3><pre><code class="java">/**
     * &lt;p&gt;
     * This is called to find out how big a view should be. The parent
     * supplies constraint information in the width and height parameters.
     * &lt;/p&gt;
     *
     * &lt;p&gt;
     * The actual measurement work of a view is performed in
     * &#123;@link #onMeasure(int, int)&#125;, called by this method. Therefore, only
     * &#123;@link #onMeasure(int, int)&#125; can and must be overridden by subclasses.
     * &lt;/p&gt;
     *
     *
     * @param widthMeasureSpec Horizontal space requirements as imposed by the
     *        parent
     * @param heightMeasureSpec Vertical space requirements as imposed by the
     *        parent
     *
     * @see #onMeasure(int, int)
     */
public final void measure(int widthMeasureSpec, int heightMeasureSpec) &#123;
    ...
    // measure ourselves, this should set the measured dimension flag back
     onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
&#125;</code></pre>
<p>可见view的measure()方法是final，不可被子类重写。里面调用onMeasure()，实际真正的测量过程在onMeasure()中。所以<strong>只有onMeasure()可以且必须被子类重写</strong>。另外，参数widthMeasureSpec、heightMeasureSpec就是上一节最后的表格中的值。继续看onMeasure()：</p>
<pre><code class="java">protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) &#123;
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    &#125;</code></pre>
<p>从名字就可以看出，<strong>setMeasuredDimension()就是设置测量的尺寸，且在onMeasure()中必须被调用</strong>，否则在测量时会发送异常。getDefaultSize()获取默认的宽/高。所以<strong>View类</strong>中的onMeasure() 是设置默认的宽高。 继续看getDefaultSize()具体实现：</p>
<pre><code class="java">public static int getDefaultSize(int size, int measureSpec) &#123;
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) &#123;
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        &#125;
        return result;
    &#125;</code></pre>
<p><strong>UNSPECIFIED</strong>，一般是系统使用，不需要关心。这里view大小直接取size，就是getSuggestedMinimumWidth()/getSuggestedMinimumHeight()，意思是 <strong>建议的 最小宽高</strong>。看下实现：</p>
<pre><code class="java">protected int getSuggestedMinimumWidth() &#123;
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    &#125;</code></pre>
<p>没有背景，就取mMinWidth，就是xml中设置的minWidth属性值；有背景，取 mMinWidth 、背景的MinimumWidth 的较大值。drawable的getMinimumWidth()如下，有固有宽度就取固有宽度（如BitmapDrawable），没有就是0（如ShadeDrawable）。</p>
<pre><code class="java">    public int getMinimumWidth() &#123;
        final int intrinsicWidth = getIntrinsicWidth();
        return intrinsicWidth &gt; 0 ? intrinsicWidth : 0;
    &#125;</code></pre>
<p><strong>AT_MOST、EXACTLY</strong>，直接取specSize，就是上一节最后的表格中的值，作为测量宽高。<strong>那这样取specSize是否合适呢</strong>？ 再来看一遍specSize的来源。</p>
<table>
<thead>
<tr>
<th>parentSpecMode /childLayoutParams</th>
<th>EXACTLY</th>
<th>AT_MOST</th>
<th>UNSPECIFIED</th>
</tr>
</thead>
<tbody><tr>
<td>dp/px</td>
<td>1EXACTLY childSize</td>
<td>2EXACTLY childsize</td>
<td>EXACTLY childsize</td>
</tr>
<tr>
<td>match_parent</td>
<td>3EXACTLY parentSize</td>
<td>4<strong>AT_MOST parentSize</strong></td>
<td>UNSPECIFIED 0</td>
</tr>
<tr>
<td>wrap_content</td>
<td>5<strong>AT_MOST parentSize</strong></td>
<td>6<strong>AT_MOST parentSize</strong></td>
<td>UNSPECIFIED 0</td>
</tr>
</tbody></table>
<p><strong>1、2的情况</strong>，具体dp值，取SpecSize没问题，因为是EXACTLY，就是给定的的尺寸。 <strong>3的情况</strong>，match_parent，取SpecSize，即parentSize，也没问题，因为是EXACTLY，也是确定的尺寸。 <strong>4的情况</strong>，match_parent，但父容器又是wrap_content，系统就给了AT_MOST+parentSize，限制最大尺寸为parentSize。而这里直接取specSize即parentSize，似乎也没问题。这个看一个<strong>例子一</strong>，如下，view是match_parent，可见view取得确实是parentSize。</p>
<p><strong>5、6的情况</strong>，wrapContent即AT_MOST+parentSize，取specSize也就是parentSize，所以和3、4一样都是parentSize，即 View类 中 默认wrapContent等同于match_parent。</p>
<p>再看一个情况<strong>例子二</strong>，如下，View换成TextView（继承View），尺寸就不是parentSize了，而是内容尺寸，说明TextView在onMeasure中做了处理。</p>
<p>继续看，<strong>例子三</strong>如下，同时有TextView、View，此时textView又是取parentSize（可用空间）：</p>
<p><img src="http://user-gold-cdn.xitu.io/2020/7/20/1736bb003a5a1fec?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="在这里插入图片描述"></p>
<p>所以得出结论： 通常<strong>直接继承View的自定义View</strong>，在onMeasure()需要处理 ： a、wrap_content的情况，否则wrap_content就等同于match_parent； b、match_parent+父容器wrap_content的情况，否则就像<strong>例子一</strong>，父容器wrap_content是无效的，处理方式就是<strong>例子二</strong>中的textView。 总结就是，<strong>直接继承View的自定义View，需要处理AT_MOST时的宽高</strong>。</p>
<p><strong>处理方式</strong>如下：</p>
<pre><code class="java">@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) &#123;
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthMode == MeasureSpec.AT_MOST &amp;&amp; heightMode == MeasureSpec.AT_MOST) &#123;
            setMeasuredDimension(mWidth, mHeight);
        &#125; else if (widthMode == MeasureSpec.AT_MOST) &#123;
            setMeasuredDimension(mWidth, heightSize);
        &#125; else if (heightMode == MeasureSpec.AT_MOST) &#123;
            setMeasuredDimension(widthSize, mHeight);
        &#125;
    &#125;</code></pre>
<p>实际就是<strong>在 AT_MOST时 设置一个指定的尺寸mWidth、mHeight，其他情况沿用系统。至于mWidth、mHeight是多少，则要具体看你的view的逻辑了</strong>。例如TextView，可以参考其源码的实现。</p>
<h3 id="3-1-2-ViewGroup的测量过程"><a href="#3-1-2-ViewGroup的测量过程" class="headerlink" title="3.1.2 ViewGroup的测量过程"></a>3.1.2 ViewGroup的测量过程</h3><blockquote>
<p>ViewGroup需要完成自身的测量，还要遍历子view调用measure()方法进行测量。</p>
</blockquote>
<p>ViewGroup是抽象类，没有重写onMeasure，因为无法做到统一，是让具体继承ViewGroup的子类重写自己的逻辑。但是提供一些方便的方法给子类调用。如measureChildren()、measureChild()、measureChildWithMargins()，上面第二节分析过measureChildWithMargins()，这里我们看下measureChildren()：</p>
<pre><code class="java">/**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this view
     */
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) &#123;
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i &lt; size; ++i) &#123;
            final View child = children[i];
            if ((child.mViewFlags &amp; VISIBILITY_MASK) != GONE) &#123;
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            &#125;
        &#125;
    &#125;</code></pre>
<p>就是遍历子view，调用measureChild()，继续看：</p>
<pre><code class="java">protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) &#123;
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    &#125;</code></pre>
<p>通过getChildMeasureSpec()获取child的MeasureSpec，然后调用child.measure()，测量就传到child内部了，很好理解。measureChild()相比measureChildWithMargins() 没有考虑child的margin值。</p>
<p>上面说了，ViewGroup没有重写onMeasure，因为无法做到统一，让具体继承ViewGroup的子类重写自己的逻辑。具体<strong>看下LinearLayout的测量过程</strong>。</p>
<pre><code class="java">@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) &#123;
        if (mOrientation == VERTICAL) &#123;
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        &#125; else &#123;
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        &#125;
    &#125;</code></pre>
<p>继续看measureVertical()：</p>
<pre><code class="java">void measureVertical(int widthMeasureSpec, int heightMeasureSpec) &#123;
    ...
    //下面这句官方注释：看每个人多高，也记住最大宽度。想想这不就是计算竖向LinearLayout宽高的思路嘛！
    // See how tall everyone is. Also remember max width.
        for (int i = 0; i &lt; count; ++i) &#123;
            ...
            final View child = getVirtualChildAt(i);
            ...
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            ...
                // Determine how big this child would like to be. If this or
                // previous children have given a weight, then we allow it to
                // use all available space (and we will shrink things later
                // if needed).
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                //这里测量child（里面就是measureChildWithMargins()）
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight();
                ...
                final int totalLength = mTotalLength;
                //这里mTotalLength加上child的高度、margin，就是child高度累积。
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));
                ...
                //这里记录最大宽度（包含margin）
                final int margin = lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);
            ...
        &#125;
        //遍历完了：高度加上自身的上下padding
        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;
        int heightSize = mTotalLength;
        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        //这里很重要：调用resolveSizeAndState--决定 计算的高度（高度累加）和 LinearLayout的父容器约束的高度，取哪一个。
        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState &amp; MEASURED_SIZE_MASK;
    ...
        //最大宽度加上左右margin
        maxWidth += mPaddingLeft + mPaddingRight;

        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        //设置最终的测量尺寸（宽也也同样调用resolveSizeAndState决定取哪个）
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
&#125;</code></pre>
<p>所以，简单概括就是： 1.先测量所有child； 2.根据child的情况获取自身宽高（累加高度、最大宽度）。</p>
<p><strong>那么，是否就取 累加高度、最大宽度？再看下resolveSizeAndState()</strong>:</p>
<pre><code class="java">/**
     * Utility to reconcile a desired size and state, with constraints imposed
     * by a MeasureSpec. Will take the desired size, unless a different size
     * is imposed by the constraints. The returned value is a compound integer,
     * with the resolved size in the &#123;@link #MEASURED_SIZE_MASK&#125; bits and
     * optionally the bit &#123;@link #MEASURED_STATE_TOO_SMALL&#125; set if the
     * resulting size is smaller than the size the view wants to be.
     *
     * @param size How big the view wants to be. --想要的尺寸
     * @param measureSpec Constraints imposed by the parent. --父布局给的measureSpec
     * @param childMeasuredState Size information bit mask for the view&#39;s
     *                           children.
     * @return Size information bit mask as defined by
     *         &#123;@link #MEASURED_SIZE_MASK&#125; and
     *         &#123;@link #MEASURED_STATE_TOO_SMALL&#125;.
     */
    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) &#123;
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) &#123;
            case MeasureSpec.AT_MOST:
                //AT_MOST时，想要的尺寸大于约束的尺寸，就只能取 约束的尺寸。
                if (specSize &lt; size) &#123;
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                &#125; else &#123;
                    result = size;
                &#125;
                break;
            case MeasureSpec.EXACTLY:
                //dp值、match_parent且父EXACTLY，就是SpecSize
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        &#125;
        return result | (childMeasuredState &amp; MEASURED_STATE_MASK);
    &#125;</code></pre>
<p>这个过程就是 限制 AT_MOST时，即<strong>wrap_content</strong>（或match_parent且父wrap_content）时高度不能大于parent的剩余空间。</p>
<h3 id="3-1-3-获取View宽高的时机"><a href="#3-1-3-获取View宽高的时机" class="headerlink" title="3.1.3 获取View宽高的时机"></a>3.1.3 获取View宽高的时机</h3><blockquote>
<p>Measure过程完成，就可通过getMeasuredWidth()、getMeasuredHeight()获取测量宽高。但某些极端情况 需要多次Measure才能确定最终宽高。所以在onLayout方法中获取测量宽高是真正ok的。 我们知道，activity的onCreate中无法获取到view的宽高。实际onCreate、onStart、onResume都不能保证view已完成测量，所以可能获取的都是0。因为view的measure和activity生命周期不是同步的。</p>
</blockquote>
<p>以下是<strong>保证可以获取view测量宽高的方法</strong>：</p>
<h4 id="1、Activity-View-onWindowFocusChanged"><a href="#1、Activity-View-onWindowFocusChanged" class="headerlink" title="1、Activity/View # onWindowFocusChanged"></a>1、Activity/View # onWindowFocusChanged</h4><p>onWindowFocusChanged：View已初始化完毕，宽高已准备ok。 但会多次调用，获取焦点、失去焦点都回调用。(这个回调是ViewRootIml中分发到DecorView，接着到Activity、到各级View。)</p>
<pre><code class="java">@Override
    public void onWindowFocusChanged(boolean hasFocus) &#123;
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) &#123;
            int measuredWidth = scoreView.getMeasuredWidth();
            int measuredHeight = scoreView.getMeasuredHeight();
        &#125;
    &#125;</code></pre>
<h4 id="2、view-post-runnable"><a href="#2、view-post-runnable" class="headerlink" title="2、view.post(runnable)"></a>2、view.post(runnable)</h4><p>view.post可以把runnable放入消息队列，等待looper到此runnable是view已经初始化完成。v详细原理参考<a target="_blank" rel="noopener" href="https://www.cnblogs.com/dasusu/p/8047172.html">【Android源码解析】View.post()到底干了啥</a></p>
<pre><code class="java">@Override
    protected void onStart() &#123;
        super.onStart();
        scoreView.post(new Runnable() &#123;
            @Override
            public void run() &#123;
                int measuredWidth = scoreView.getMeasuredWidth();
                int measuredHeight = scoreView.getMeasuredHeight();
            &#125;
        &#125;);
    &#125;</code></pre>
<h4 id="3、ViewTreeObserver"><a href="#3、ViewTreeObserver" class="headerlink" title="3、ViewTreeObserver"></a>3、ViewTreeObserver</h4><p>ViewTreeObserver有很多回调，其中有个OnGlobalLayoutListener，当View树的状态发生改变或者View树内部view的可见性发生改变时 方法 onGlobalLayout()都会被调用。所以是会回调多次。 此时也可以获取view的宽高：</p>
<pre><code class="java">ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() &#123;
            @Override
            public void onGlobalLayout() &#123;
                mDefaultControlLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                if (mIsGroupListAnimating) &#123;
                    mIsGroupListAnimationPending = true;
                &#125; else &#123;
                    updateLayoutHeightInternal(animate);
                &#125;
            &#125;
        &#125;);</code></pre>
<h2 id="3-2Layout过程"><a href="#3-2Layout过程" class="headerlink" title="3.2Layout过程"></a>3.2Layout过程</h2><p>layout()的作用是View用来确定view本身位置，内部调用onLayout()来确定子view的位置。 layout过程比measure过程简单很多。看View的layout方法：</p>
<pre><code class="java">    public void layout(int l, int t, int r, int b) &#123;
        if ((mPrivateFlags3 &amp; PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) &#123;
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &amp;= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        &#125;

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        //使用setFrame方法设置4个顶点，就确定位置了~
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags &amp; PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) &#123;
            //这里调用onLayout，是个空实现。ViewGroup中重写了，还是空实现，但加了abstract，即ViewGroup的子类必须重写onLayout确定子View的位置。
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) &#123;
                if(mRoundScrollbarRenderer == null) &#123;
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                &#125;
            &#125; else &#123;
                mRoundScrollbarRenderer = null;
            &#125;

            mPrivateFlags &amp;= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null &amp;&amp; li.mOnLayoutChangeListeners != null) &#123;
                ArrayList&lt;OnLayoutChangeListener&gt; listenersCopy =
                        (ArrayList&lt;OnLayoutChangeListener&gt;)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i &lt; numListeners; ++i) &#123;
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                &#125;
            &#125;
        &#125;

        ...
    &#125;</code></pre>
<p><strong>先是用setFrame方法设置4个顶点，就确定位置了，即mLeft、mTop、mBottom、mRight确定了。</strong> 然后调用onLayout，是个空实现。ViewGroup中重写了onLayout，还是空实现，但加了abstract，即<strong>ViewGroup的子类必须重写onLayout确定子View的位置</strong>。 那就<strong>看看LinearLayout的onLayout</strong>：</p>
<pre><code class="java">@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) &#123;
        if (mOrientation == VERTICAL) &#123;
            layoutVertical(l, t, r, b);
        &#125; else &#123;
            layoutHorizontal(l, t, r, b);
        &#125;
    &#125;</code></pre>
<p>继续看layoutVertical()：</p>
<pre><code class="java">void layoutVertical(int left, int top, int right, int bottom) &#123;
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity &amp; Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity &amp; Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        switch (majorGravity) &#123;
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        &#125;
        //遍历子view
        for (int i = 0; i &lt; count; i++) &#123;
            final View child = getVirtualChildAt(i);
            if (child == null) &#123;
                childTop += measureNullChild(i);
            &#125; else if (child.getVisibility() != GONE) &#123;
                //获取child的测量宽高
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity &lt; 0) &#123;
                    gravity = minorGravity;
                &#125;
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity &amp; Gravity.HORIZONTAL_GRAVITY_MASK) &#123;
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                &#125;

                if (hasDividerBeforeChildAt(i)) &#123;
                    childTop += mDividerHeight;
                &#125;

                childTop += lp.topMargin;
                //以上就是获取子view的左、上的位置，即宽高，然后调用setChildFrame
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                 //top位置加上高度和margin，就是下一个view的top
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            &#125;
        &#125;
    &#125;</code></pre>
<p>就是遍历子view，确认childLeft、childTop，调用setChildFrame确认子view的位置：</p>
<pre><code class="java">private void setChildFrame(View child, int left, int top, int width, int height) &#123;
        //这里width、height就是 上面获取的 测量宽高
        child.layout(left, top, left + width, top + height);
    &#125;</code></pre>
<p>也就是调用child的layout方法，这样就走child的layout过程了。</p>
<p><strong>一个问题：getMeasuredWidth() 与 getWidth()有何区别？</strong> 答曰：<strong>一般情况，getMeasuredWidth() 与 getWidth()两者无区别</strong>。 先看，getWidth():</p>
<pre><code class="java">public final int getWidth() &#123;
        return mRight - mLeft;
    &#125;</code></pre>
<p>在上面分析LinearLayout时，child.layout的参数中 mRight就是mLeft + measuredWidth，所以getWidth()就是measuredWidth。只不过是measuredWidth在测量过程产生，getWidth()在layout过程产生。 只要不重写view的layout()方法（也不需要重写）改变顶点位置就不会出现不同的情况，例如下面这个最终宽高比测量宽高大100。</p>
<pre><code class="java">    public void layout(int l, int t, int r, int b) &#123;
        super.layout(l,t,r+100,b+100);
    &#125;</code></pre>
<h2 id="3-3Draw过程"><a href="#3-3Draw过程" class="headerlink" title="3.3Draw过程"></a>3.3Draw过程</h2><p>draw过程： 1、画背景 2、画自己– onDraw，自己实现 3、画子view– dispatchDraw 4、画装饰</p>
<pre><code class="java">public void draw(Canvas canvas) &#123;
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags &amp; PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &amp;&amp;
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags &amp; ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas&#39; layers to prepare for fading
         *      3. Draw view&#39;s content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) &#123;
            drawBackground(canvas);
        &#125;

        // skip step 2 &amp; 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags &amp; FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags &amp; FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges &amp;&amp; !horizontalEdges) &#123;
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null &amp;&amp; !mOverlay.isEmpty()) &#123;
                mOverlay.getOverlayView().dispatchDraw(canvas);
            &#125;

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) &#123;
                debugDrawFocus(canvas);
            &#125;

            // we&#39;re done...
            return;
        &#125;</code></pre>
<p>ViewGroup一般不用onDraw画自己，只需要画子View就可以了。但明确需要画自己的话，需要调用setViewNotDraw(false);</p>
<p>以上View的三大流程就分析完了。</p>
<h1 id="4、自定义View"><a href="#4、自定义View" class="headerlink" title="4、自定义View"></a>4、自定义View</h1><blockquote>
<p>自定义view涉及view层次结构、事件分发、工作原理，有一定复杂度，但也是有章可循的。</p>
</blockquote>
<h2 id="4-1自定义view的分类"><a href="#4-1自定义view的分类" class="headerlink" title="4.1自定义view的分类"></a>4.1自定义view的分类</h2><ol>
<li><strong>继承View</strong>：重写onDraw，要处理wrap_content、padding。</li>
<li><strong>继承ViewGroup</strong>：重写onMeasure测量自己、子View，重写onLayout布局子View。</li>
<li><strong>继承特定View（如TextView）</strong>：扩展自己的功能。</li>
<li><strong>继承特定ViewGroup（如LinearLayout）</strong>：扩展自己的功能。</li>
</ol>
<h2 id="4-2-自定义view-注意点"><a href="#4-2-自定义view-注意点" class="headerlink" title="4.2 自定义view 注意点"></a>4.2 自定义view 注意点</h2><ol>
<li><strong>支持wrap_content</strong>：直接继承View或ViewGroup的，要在onMeasure中处理wrap_content的情况。</li>
<li><strong>支持padding</strong>：直接继承View在onDraw中处理；直接继承ViewGroup，在onMeasure、onLayout中处理padding和子view的margin。</li>
<li><strong>不要在View中使用handler</strong>，因为本身提供了post方法。</li>
<li><strong>在View#onDetachedFromWindow中停止动画或线程。</strong></li>
<li><strong>处理好嵌套滑动。</strong></li>
</ol>
<h2 id="4-3-例子"><a href="#4-3-例子" class="headerlink" title="4.3 例子"></a>4.3 例子</h2><p><a target="_blank" rel="noopener" href="https://blog.csdn.net/hfy8971613/article/details/81589829">自定义ViewGroup实例：横向滑动HorizontalView</a></p>
<h2 id="4-4-自定义view的思想"><a href="#4-4-自定义view的思想" class="headerlink" title="4.4 自定义view的思想"></a>4.4 自定义view的思想</h2><p>先掌握基本功，弹性滑动、滑动冲突、绘制原理等，然后选择自定义的类别，按照注意事项多做就可以了。</p>

        </div>

    </div>

    

    

    

    

    

    
<nav class="article-nav">
  
    <a href="/2020/07/27/Android%E5%85%A8%E6%96%B0UI%E7%BC%96%E7%A8%8B-Jetpack-Compose-%E8%B6%85%E8%AF%A6%E7%BB%86%E6%95%99%E7%A8%8B-%E7%AC%AC1%E5%BC%B9/" id="article-nav-newer" class="article-nav-link-wrap">
      <div class="article-nav-caption">下一篇</div>
      <div class="article-nav-title">
        
          Android全新UI编程 - Jetpack Compose 超详细教程 第1弹
        
      </div>
    </a>
  
  
    <a href="/2020/07/05/%E7%94%A8%E5%8D%8F%E7%A8%8B%E6%8E%A7%E5%88%B6%E5%A4%9A%E4%B8%AA%E5%B9%B6%E8%A1%8C%E5%BC%82%E6%AD%A5%E7%BB%93%E6%9E%9C%E7%9A%84%E4%BC%98%E5%85%88%E7%BA%A7/" id="article-nav-older" class="article-nav-link-wrap">
      <div class="article-nav-caption">上一篇</div>
      <div class="article-nav-title">用协程控制多个并行异步结果的优先级</div>
    </a>
  
</nav>


    <section class="share">
        <div class="share-title">分享</div>
        <a class="share-item" target="_blank"
            href="https://twitter.com/share?text=Android进阶基础系列：View的工作原理 全面理解！ - ApocalypseBlog&url=http://example.com/2020/07/25/Android%E8%BF%9B%E9%98%B6%E5%9F%BA%E7%A1%80%E7%B3%BB%E5%88%97%EF%BC%9AView%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86-%E5%85%A8%E9%9D%A2%E7%90%86%E8%A7%A3%EF%BC%81/">
            <box-icon type='logo' name='twitter'></box-icon>
        </a>
        <a class="share-item" target="_blank"
            href="https://www.facebook.com/sharer.php?title=Android进阶基础系列：View的工作原理 全面理解！ - ApocalypseBlog&u=http://example.com/2020/07/25/Android%E8%BF%9B%E9%98%B6%E5%9F%BA%E7%A1%80%E7%B3%BB%E5%88%97%EF%BC%9AView%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86-%E5%85%A8%E9%9D%A2%E7%90%86%E8%A7%A3%EF%BC%81/">
            <box-icon name='facebook-square' type='logo' ></box-icon>
        </a>
        <!-- <a class="share-item" target="_blank"
            href="https://service.weibo.com/share/share.php?title=Android进阶基础系列：View的工作原理 全面理解！ - ApocalypseBlog&url=http://example.com/2020/07/25/Android%E8%BF%9B%E9%98%B6%E5%9F%BA%E7%A1%80%E7%B3%BB%E5%88%97%EF%BC%9AView%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86-%E5%85%A8%E9%9D%A2%E7%90%86%E8%A7%A3%EF%BC%81/&pic=">
            <div class="n-icon n-icon-weibo"></div>
        </a> -->
    </section>

</article>









</div>
                </section>
            </section>

             
            <aside class="sidebar">
            
                
            </aside>
        </div>
    </div>

    <footer class="footer">
    <div class="footer-wave">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="#3c4859" fill-opacity="1" d="M0,160L60,181.3C120,203,240,245,360,240C480,235,600,181,720,186.7C840,192,960,256,1080,261.3C1200,267,1320,213,1380,186.7L1440,160L1440,320L1380,320C1320,320,1200,320,1080,320C960,320,840,320,720,320C600,320,480,320,360,320C240,320,120,320,60,320L0,320Z"></path></svg>
    </div>

    <div class="footer-wrap">
        <div class="footer-inner"> 
            ApocalypseBlog &copy; 2021<br>
            Powered By Hexo · Theme By <a href="https://github.com/lh1me/hexo-theme-aomori" target="_blank">Aomori</a>
        </div>
    </div>

</footer>




<script src="/dist/build.js?1.14.0.js"></script>


<script src="/dist/custom.js?1.14.0.js"></script>









</body>

</html>